--[[
Copyright (C) 2008-2009 Adirelle

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--]]

if not Yatba then return end
local Yatba = Yatba

local pairs, next, type, error, assert = pairs, next, type, error, assert
local band, bor = bit.band, bit.bor

local bars, groups, watchedGUIDs = Yatba.bars, Yatba.groups, Yatba.watchedGUIDs
local L, new, del, erase =  Yatba.L, Yatba.new, Yatba.del, Yatba.erase

--------------------------------------------------------------------------------
-- Database upvalue
--------------------------------------------------------------------------------

local db
Yatba.RegisterSignal('FiltersDB', 'DatabaseSet', function() db = Yatba.db.profile end)

--------------------------------------------------------------------------------
-- Local functions
--------------------------------------------------------------------------------

local GetAuraName, BuildAuraRuleTable, BuildFilterCode, GuessAuraId, GetAuraList
local ApplyAuraRule

--------------------------------------------------------------------------------
-- Group filters
--------------------------------------------------------------------------------

local unitFilterFlags = {
	player   = 0x01,
	pet      = 0x02,
	party    = 0x04,
	raid     = 0x08,
	friendly = 0x10,
	hostile  = 0x20,
}

local allUnits = 0x3F

local unitFilterConditions = {
	-- fields : includeMask, includeMatch, matchCode[, excludeMask[, excludeCode] ]
	hostile  = { 0x20, 0x20, "UnitCanAttack('player', unitId)" },
	friendly = { 0x10, 0x10, "UnitIsFriend(unitId, 'player')" },
	raid     = { 0x18, 0x08, "UnitPlayerOrPetInRaid(unitId)",  0x10 },
	party    = { 0x1C, 0x04, "UnitPlayerOrPetInParty(unitId)", 0x18 },
	pet      = { 0x1E, 0x02, "UnitIsUnit(unitId, 'pet')",      0x1C },
	player   = { 0x1D, 0x01, "UnitIsUnit(unitId, 'player')",   0x1C },
}

local filterCode = {}
local unitTests = {}
local tmp = {}

function BuildFilterCode(group)
	local groupDB = group.db
	assert(groupDB, ("No DB in group %s"):format(group.name or group))

	if not groupDB.auraTypes.BUFF and not groupDB.auraTypes.DEBUFF then
		return "false"
	end

	erase(filterCode)

	if groupDB.auraTypes.BUFF and not groupDB.auraTypes.DEBUFF then
		tinsert(filterCode, "(auraType == 'BUFF')")
	elseif groupDB.auraTypes.DEBUFF and not groupDB.auraTypes.BUFF then
		tinsert(filterCode, "(auraType == 'DEBUFF')")
	end

	group.auraRuleTable = BuildAuraRuleTable(group)
	if db.advancedFiltering then
		if group.auraRuleTable then
			tinsert(filterCode, "ApplyAuraRule(group.auraRuleTable[auraName:lower()], isMine)")
		else
			tinsert(filterCode, "ApplyAuraRule(nil, isMine)")
		end
	else -- Defaults to "mine"
		tinsert(filterCode, "isMine")
	end

	if groupDB.durationFilter then
		if groupDB.minDuration > 0 then
			tinsert(filterCode, ("(duration >= %d)"):format(groupDB.minDuration))
		end
		if groupDB.maxDuration > 0 then
			if groupDB.maxDuration < groupDB.minDuration then
				return "false"
			end
			tinsert(filterCode, ("(duration <= %d)"):format(groupDB.maxDuration))
		end
	end

	local mask = 0
	for key, flag in pairs(unitFilterFlags) do
		if groupDB.units[key] then
			mask = bor(mask, unitFilterFlags[key])
		end
	end
	if mask == 0 then
		return "false"
	elseif mask ~= allUnits then
		erase(unitTests)
		for key, info in pairs(unitFilterConditions) do
			local includeMask, includeMatch, matchCode, excludeMask, excludeCode = unpack(info)
			if band(mask, includeMask) == includeMatch then
				-- Exact match, add the code
				tinsert(unitTests, "("..matchCode..")")
			elseif band(mask, includeMatch) == 0 and excludeMask then
				-- Not included but but may be actively excluded from other filters
				if band(mask, excludeMask) > 0 then
					tinsert(filterCode, excludeCode or ("(not (%s))"):format(matchCode))
				end
			end
		end
		if #unitTests > 0 then
			tinsert(filterCode, "("..table.concat(unitTests, " or ")..")")
		end
	end

	if #filterCode == 0 then
		return "true"
	end

	return table.concat(filterCode, "\nand ")
end

local filterFuncEnv
local compileCache = setmetatable({}, { __index = function(self, code)
	if not code then
		return
	end
	local fullCode = ("return function(group, unitId, auraName, auraType, duration, isMine) return %s end"):format(code)
	local success, compiled = pcall(loadstring, fullCode, code)
	if not success then
		error(("Error compiling %q: %s"):format(fullCode, compiled))
		return
	end
	local filterFunc
	success, filterFunc = pcall(compiled)
	if not success then
		error(("Error running %q: %s"):format(fullCode, filterFunc))
	end
	if not filterFuncEnv then
		filterFuncEnv = setmetatable({ApplyAuraRule=ApplyAuraRule}, {__index=_G})
	end
	filterFunc = setfenv(filterFunc, filterFuncEnv)
	self[code] = filterFunc
	return filterFunc
end})

function Yatba:UpdateFilter(group)
	if type(group) == "string" then
		group = groups[group]
	end
	if type(group) ~= "table" then
		error(("Yatba:UpdateFilter: %s it not a table"):format(tostring(group)))
	end
	local code = BuildFilterCode(group)
	self:Debug("Filter for %q:\n%s", group.name, code)
	if group.filterCode ~= code then
		group.filterCode = code
		group.AcceptTimer = compileCache[code]
	end
	self:SendSignal('FilterUpdated_'..group.name)
end

--------------------------------------------------------------------------------
-- Aura filter handling
--------------------------------------------------------------------------------

local auraRuleTables = {}

function BuildAuraRuleTable(group)
	local ruleTable = auraRuleTables[group]
	if db.advancedFiltering and next(db.auraRules) then
		if not ruleTable then
			ruleTable = new()
		else
			erase(ruleTable)
		end
		Yatba:Debug('Updating %q by-name filter', group.name)
		for id,rule in pairs(db.auraRules) do
			local name = GetAuraName(id):lower()
			if rule == 'group' then
				ruleTable[name] = group.db.auraRules[id]
			elseif rule ~= 'default' then
				ruleTable[name] = rule
			end
			Yatba:Debug('- %q: %s', name, ruleTable[name])
		end
	elseif ruleTable then
		ruleTable = del(ruleTable)
	end
	auraRuleTables[group] = ruleTable
	return ruleTable
end

function ApplyAuraRule(rule, isMine)
	rule = rule or db.defaultRule
	return (rule == 'all') or (rule == 'mine' and isMine)
end

local spellNameCache = setmetatable({}, {__mode='kv'})

function GuessAuraId(input)
	input = input and tostring(input):trim()
	if not input or input == "" then
		return
	end
	if input:match('item:%d+') then
		input = GetItemInfo(input)
	end
	if input:match('^%d+$') then
		return tonumber(input)
	elseif input:match('spell[:=]%d+') then
		return tonumber(input:match('spell[:=](%d+)'))
	end
	local success, link
	success, link = pcall(GetSpellLink, input)
	if success and link then
		return tonumber(link:match('spell:(%d+)'))
	end
	-- We're getting very dirty here, blame Ackis
	input = input:lower()
	if spellNameCache[input] then
		return spellNameCache[input]
	end
	for id = spellNameCache.__id or 1, 100000 do
		local name = GetSpellInfo(id)
		if name then
			name = name:lower()
			spellNameCache[name] = id
			if name == input then
				spellNameCache.__id = id + 1
				return id
			end
		end
	end
	spellNameCache.__id = id
end

function GetAuraName(id)
	return type(id) == 'number' and GetSpellInfo(id)
end

function Yatba:ValidateAuraName(input)
	local id = GuessAuraId(input)
	if not id then
		return L['Spell %q does not exist.']:format(input)
	elseif db.auraRules[id] ~= nil then
		return L['%q is already in the list.']:format(input)
	end
	return true
end

function Yatba:AddAura(name)
	local id = GuessAuraId(name)
	db.auraRules[id] = db.defaultRule ~= 'ignore' and 'ignore' or 'mine'
	self:AddAuraOptions(id, GetAuraName(id))
	Yatba:SendSignal('AuraFilterUpdated')
end

function Yatba:RemoveAura(id)
	db.auraRules[id] = nil
	for name,group in pairs(db.groups) do
		group.auraRules[id] = nil
	end
	self:RemoveAuraOptions(id)
	Yatba:SendSignal('AuraFilterUpdated')
end

do
	local tmp = {}
	function GetAuraList(groupOnly)
		erase(tmp)
		for id,status in pairs(db.auraRules) do
			if not groupOnly or status == 'group' then
				tmp[id] = GetAuraName(id)
			end
		end
		return tmp
	end
end

function Yatba:HasGroupAuraFilter()
	for id,status in pairs(db.auraRules) do
		if status == 'group' then
			return true
		end
	end
	return false
end

function Yatba:GetGlobalAuraList()
	return GetAuraList(false)
end

function Yatba:GetGroupAuraFilterList()
	return GetAuraList(true)
end

function Yatba:InitializeAuraOptions()
	for id in pairs(db.auraRules) do
		self:AddAuraOptions(id, GetAuraName(id))
	end
end

